package tr.edu.metu.is.mega.server.validation;

import java.util.HashMap;
import java.util.Map;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Past;
import javax.validation.constraints.Size;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.PropertyDescriptor;

import tr.edu.metu.is.mega.common.ValidationAttributeValue;
import tr.edu.metu.is.mega.common.ValidationConstraint;

/**
 * Validation attribute manager.
 */
public final class ValidationAttributeManager {

    private static ValidationAttributeManager instance;

    private final Validator validator;

    // <class-name, <field-name, <validation-constraint, value>>>
    private final Map<String, Map<String, Map<ValidationConstraint, ValidationAttributeValue>>> attributeMap;

    /**
     * Default constructor.
     */
    private ValidationAttributeManager() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
        attributeMap = new HashMap<String, Map<String, Map<ValidationConstraint, ValidationAttributeValue>>>();
    }

    /**
     * Returns singleton instance of this.
     * 
     * @return singleton instance of this
     */
    public static ValidationAttributeManager getInstance() {
        if (instance == null) {
            instance = new ValidationAttributeManager();
        }
        return instance;
    }

    /**
     * Returns validation attributes of a class.
     * 
     * @param className class name
     * @return map of validation attributes
     */
    public Map<String, Map<ValidationConstraint, ValidationAttributeValue>> getValidationAttributes(String className) {
        Map<String, Map<ValidationConstraint, ValidationAttributeValue>> map = attributeMap.get(className);
        if (map == null) {
            Class<?> clazz = null;
            try {
                clazz = Class.forName(className);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                return null;
            }
            map = detectValidationAttributes(clazz);
            attributeMap.put(className, map);
        }
        return map;
    }

    /**
     * Detects validation attributes of a class.
     * 
     * @param clazz class
     * @return validation attributes
     */
    private Map<String, Map<ValidationConstraint, ValidationAttributeValue>> detectValidationAttributes(Class<?> clazz) {
        BeanDescriptor constraintsForClass = validator.getConstraintsForClass(clazz);
        Map<String, Map<ValidationConstraint, ValidationAttributeValue>> ret = new HashMap<String, Map<ValidationConstraint, ValidationAttributeValue>>();
        for (PropertyDescriptor pd : constraintsForClass.getConstrainedProperties()) {
            Map<ValidationConstraint, ValidationAttributeValue> map = new HashMap<ValidationConstraint, ValidationAttributeValue>();
            for (ConstraintDescriptor<?> cd : pd.getConstraintDescriptors()) {
                if (cd.getAnnotation() instanceof Size) {
                    Size size = (Size) cd.getAnnotation();
                    int minSize = size.min();
                    if (minSize > 0) {
                        map.put(ValidationConstraint.MIN_LENGTH, new ValidationAttributeValue(minSize));
                    }
                    int maxSize = size.max();
                    if (maxSize < Integer.MAX_VALUE) {
                        map.put(ValidationConstraint.MAX_LENGTH, new ValidationAttributeValue(maxSize));
                    }
                } else if (cd.getAnnotation() instanceof Min) {
                    Min min = (Min) cd.getAnnotation();
                    long minValue = min.value();
                    map.put(ValidationConstraint.MIN_VALUE, new ValidationAttributeValue(minValue));
                } else if (cd.getAnnotation() instanceof Max) {
                    Max max = (Max) cd.getAnnotation();
                    long maxValue = max.value();
                    map.put(ValidationConstraint.MAX_VALUE, new ValidationAttributeValue(maxValue));
                } else if (cd.getAnnotation() instanceof DecimalMin) {
                    DecimalMin min = (DecimalMin) cd.getAnnotation();
                    String minValue = min.value();
                    map.put(ValidationConstraint.MIN_VALUE, new ValidationAttributeValue(minValue));
                } else if (cd.getAnnotation() instanceof DecimalMax) {
                    DecimalMax max = (DecimalMax) cd.getAnnotation();
                    String maxValue = max.value();
                    map.put(ValidationConstraint.MAX_VALUE, new ValidationAttributeValue(maxValue));
                } else if (cd.getAnnotation() instanceof AssertFalse) {
                    map.put(ValidationConstraint.BOOLEAN_VALUE, new ValidationAttributeValue(false));
                } else if (cd.getAnnotation() instanceof AssertTrue) {
                    map.put(ValidationConstraint.BOOLEAN_VALUE, new ValidationAttributeValue(true));
                } else if (cd.getAnnotation() instanceof Past) {
                    map.put(ValidationConstraint.PAST_DATE, new ValidationAttributeValue(true));
                } else if (cd.getAnnotation() instanceof Future) {
                    map.put(ValidationConstraint.FUTURE_DATE, new ValidationAttributeValue(true));
                } else if (cd.getAnnotation() instanceof Null) {
                    map.put(ValidationConstraint.NULL, new ValidationAttributeValue(true));
                } else if (cd.getAnnotation() instanceof NotNull) {
                    map.put(ValidationConstraint.NOT_NULL, new ValidationAttributeValue(true));
                }
            }
            ret.put(pd.getPropertyName(), map);
        }
        return ret;
    }

}
